use crate::{
    error::DecompressError,
    hashtable::HashTable,
    window::{Window, MAX_MATCH_LEN},
};
use napi_derive_ohos::napi;
use napi_ohos::bindgen_prelude::Buffer;

#[napi]
pub fn compress(data: &[u8]) -> Buffer {
    let mut out = Vec::with_capacity(data.len());
    let mut window = Window::new();
    let mut table = HashTable::new();
    let mut offset = 0;
    while offset < data.len() {
        let remainder = &data[offset..];
        if remainder.len() > 3 {
            let hash = table.hash(&remainder[..3]);
            table.insert(hash, window.position as u16);
            if let Some((distance, length)) = table.reference(hash, remainder, &window, offset) {
                if MAX_MATCH_LEN < offset && offset < data.len() - MAX_MATCH_LEN {
                    let m = distance as u16;
                    let code = 0x8000 + ((m << 3) & 0x3ff8) + ((length as u16) - 3);
                    out.extend(&code.to_be_bytes());
                    for _ in 0..length {
                        if offset + 3 < data.len() {
                            let hash = table.hash(&data[offset..offset + 3]);
                            table.insert(hash, window.position as u16);
                        }
                        window.push(data[offset]);
                        offset += 1;
                    }
                    continue;
                }
            }
        }
        let byte = data[offset];
        offset += 1;
        window.push(byte);
        if byte == b' ' && offset + 1 < data.len() && (0x40..0x80).contains(&data[offset]) {
            out.push(data[offset] ^ 0x80);
            if offset + 3 < data.len() {
                table.insert(table.hash(&data[offset..offset + 3]), offset as u16);
            }
            window.push(data[offset]);
            offset += 1;
            continue;
        }

        if byte == 0 || (byte > 8 && byte < 0x80) {
            out.push(byte);
        } else {
            let mut j = offset;
            let mut binseq = Vec::with_capacity(8);
            binseq.push(byte);

            while j < data.len() && binseq.len() < 8 {
                let ch = data[j];
                if ch == 0 || (ch > 8 && ch < 0x80) {
                    break;
                }

                binseq.push(ch);

                if j + 3 < data.len() {
                    table.insert(table.hash(&data[j..j + 3]), j as u16);
                }
                window.push(ch);

                j += 1;
            }

            out.extend(&(binseq.len() as u8).to_be_bytes());
            out.extend(&binseq);
            offset += binseq.len() - 1;
        }
    }
    Buffer::from(out)
}

#[napi]
pub fn decompress(data: &[u8]) -> napi_ohos::Result<Buffer> {
    decompress_inner(data).map_err(|e| napi_ohos::Error::from_reason(e.to_string()))
}

fn decompress_inner(data: &[u8]) -> Result<Buffer, DecompressError> {
    let len = data.len();
    let mut offset = 0;
    let mut uncompressed = Vec::new();
    while offset < len {
        let byte = data[offset];
        offset += 1;

        if (1..=8).contains(&byte) {
            if offset + byte as usize > len {
                return Err(DecompressError::NotEnoughData);
            }
            uncompressed.extend_from_slice(&data[offset..(offset + byte as usize)]);
            offset += byte as usize;
        } else if byte < 128 {
            uncompressed.push(byte);
        } else if byte >= 192 {
            uncompressed.push(b' ');
            uncompressed.push(byte ^ 0x80);
        } else if offset < len {
            offset += 1;
            if offset > len {
                return Err(DecompressError::OffsetOutsideData);
            }
            let mut lz77 = u16::from_be_bytes([data[offset - 2], data[offset - 1]]);
            lz77 &= 0x3fff;
            let lz77length = ((lz77 & 0x0007) as usize) + 3;
            let lz77offset = (lz77 >> 3) as usize;
            if lz77offset == 0 {
                let empty = vec![0u8; lz77length];
                uncompressed.extend_from_slice(&empty);
                continue;
            }
            let mut textlength = uncompressed.len();
            for _ in 0..lz77length {
                if let Some(textpos) = textlength.checked_sub(lz77offset) {
                    uncompressed.push(uncompressed[textpos]);
                    textlength += 1;
                } else {
                    return Err(DecompressError::InvalidOffset);
                }
            }
        }
    }
    Ok(Buffer::from(uncompressed))
}
